package com.zyl.blog.common.utils.Extreme;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.zyl.blog.common.domain.data.bean.Ip;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * IP工具类（基于百度qifu-api接口）
 * @version v1.0
 * @ProjectName: Personal_Blog_Server
 * @Package: com.zyl.blog.common.utils.Extreme
 * @ClassName: IpUtils
 * @Description:
 * @Author FinalFantasy
 * @Date 2025/8/11-16:13
 */
@Component
public final class IpUtils {

    /* 百度IP查询接口地址 */
    private static final String GEO_API_URL = "https://qifu-api.baidubce.com/ip/local/geo/v1/district";
    /* Redis缓存Key前缀（避免键冲突） */
    private static final String REDIS_IP_KEY_PREFIX = "ip:geo:";
    // 本地缓存（Caffeine）
    private static final Cache<String, Ip> LOCAL_CACHE = Caffeine.newBuilder()
            .maximumSize(5_000)         // 本地缓存容量（小于Redis，存储高频访问IP）
            .expireAfterWrite(10, TimeUnit.MINUTES)  // 本地缓存过期时间（比Redis短，优先更新）
            .build();

    /* Jackson解析器（用于JSON序列化/反序列化） */
    private static final ObjectMapper objectMapper = new ObjectMapper();

    /* Redis模板（注入Spring的StringRedisTemplate） */
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /* 布式锁（避免缓存击穿） */
    private final ReentrantLock lock = new ReentrantLock();

    static {
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    private IpUtils() {
        //throw new AssertionError("工具类禁止实例化");
    }

    /**
     * 查询IP地理信息（二级缓存优先：本地缓存 → Redis → 接口）
     */
    public Ip queryIpGeoInfo(String ip) throws IpGeoException, IOException {
        if (!isValidIp(ip)) {
            throw new IpGeoException("无效IP地址: " + ip);
        }

        /* 先查本地缓存（最快，微秒级） */
        Ip localIp = LOCAL_CACHE.getIfPresent(ip);
        if (localIp != null) {
            LogUtil.info("本地缓存命中，IP: [{}]", ip);
            return localIp;
        }

        /* 本地缓存未命中，查Redis分布式缓存 */
        Ip redisIp = getIpFromRedis(ip);
        if (redisIp != null) {
            LogUtil.info("Redis缓存命中，IP: [{}]", ip);
            /* 同步到本地缓存，提升下次访问速度 */
            LOCAL_CACHE.put(ip, redisIp);
            return redisIp;
        }

        /* 双缓存均未命中，加锁防止缓存击穿（高并发下重复请求接口） */
        try {
            lock.lock();
            /* 双重检查：加锁后再查一次缓存，避免锁等待期间已被其他线程更新 */
            Ip doubleCheckIp = LOCAL_CACHE.getIfPresent(ip);
            if (doubleCheckIp != null) {
                LogUtil.info("双重检查命中本地缓存，IP: [{}]", ip);
                return doubleCheckIp;
            }

            /* 调用接口获取数据（带重试） */
            Ip freshIp = fetchIpFromApiWithRetry(ip);

            /* 双写缓存：同时更新Redis和本地缓存 */
            /* 先更新Redis（保证分布式一致性） */
            saveIpToRedis(ip, freshIp);
            /* 再更新本地缓存 */
            LOCAL_CACHE.put(ip, freshIp);

            LogUtil.info("IP信息缓存成功，IP: [{}]", ip);
            return freshIp;
        } finally {
            lock.unlock();  // 确保锁释放
        }
    }

    /**
     * 从Redis获取IP信息
     */
    private Ip getIpFromRedis(String ip) {
        try {
            String key = REDIS_IP_KEY_PREFIX + ip;
            String json = stringRedisTemplate.opsForValue().get(key);
            if (json == null || json.isEmpty()) {
                return null;
            }
            return objectMapper.readValue(json, Ip.class);
        } catch (JsonProcessingException e) {
            LogUtil.error("Redis缓存反序列化失败，IP: [{}]", null, ip, e);

            /* 反序列化失败时视为未命中 */
            return null;
        }
    }

    /**
     * 将IP信息存入Redis
     */
    private void saveIpToRedis(String ip, Ip ipInfo) {
        try {
            String key = REDIS_IP_KEY_PREFIX + ip;
            String json = objectMapper.writeValueAsString(ipInfo);
            // Redis过期时间比本地缓存长（40分钟），确保本地缓存过期后仍有备份
            stringRedisTemplate.opsForValue().set(key, json, 40, TimeUnit.MINUTES);
        } catch (JsonProcessingException e) {
            /* 序列化失败不影响主流程，仅日志记录 */
            LogUtil.error("Redis缓存序列化失败，IP: [{}]", null, ip, e);
        }
    }

    /**
     * 带重试机制的接口请求（独立方法，与缓存逻辑分离）
     */
    private Ip fetchIpFromApiWithRetry(String ip) throws IpGeoException, IOException {
        int maxRetries = 2;
        int retryCount = 0;

        while (retryCount <= maxRetries) {
            try {
                String url = GEO_API_URL + (ip != null && !ip.isEmpty() ? "?ip=" + ip : "");
                String response = sendGetRequest(url);

                BaiduIpResponse apiResponse = objectMapper.readValue(response, BaiduIpResponse.class);
                if (!"Success".equals(apiResponse.getCode())) {
                    throw new IpGeoException("IP接口返回失败: " + apiResponse.getData());
                }

                Ip ipInfo = convertToIp(apiResponse);
                if (ipInfo.getIp() == null || ipInfo.getIp().isEmpty()) {
                    throw new IpGeoException("IP信息解析失败，响应为空");
                }
                return ipInfo;
            } catch (IpGeoException e) {
                if (e.getCause() instanceof SocketTimeoutException && retryCount < maxRetries) {
                    retryCount++;
                    LogUtil.warn("IP查询超时，开始第 [{}] 次重试，IP: [{}]", retryCount, ip);
                    continue;
                }
                throw e;
            }
        }
        throw new IpGeoException("IP查询多次超时，已达最大重试次数，IP: " + ip);
    }

    /**
     * 获取客户端真实IP地址（处理代理场景）
     */
    public String getClientIp(HttpServletRequest request) {
        Objects.requireNonNull(request, "HttpServletRequest不能为null");

        String[] proxyHeaders = {
                "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
                "HTTP_CLIENT_IP", "X-Real-IP"
        };

        for (String header : proxyHeaders) {
            String ip = request.getHeader(header);
            if (isValidIp(ip)) {
                return ip.contains(",") ? ip.split(",")[0].trim() : ip.trim();
            }
        }
        String clientIp = request.getRemoteAddr();
        return normalizeIp(clientIp);
    }

    /**
     * 从HTTP请求中直接获取IP地理信息（优先走缓存）
     */
    public Ip getGeoInfoFromRequest(HttpServletRequest request) throws IpGeoException, IOException {
        String clientIp = getClientIp(request);
        return queryIpGeoInfo(clientIp);
    }

    /**
     * 发送HTTP GET请求
     */
    private String sendGetRequest(String url) throws IOException, IpGeoException {
        HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(10000);
        conn.setRequestProperty("Accept", "application/json");

        int responseCode;
        try {
            responseCode = conn.getResponseCode();
        } catch (SocketTimeoutException e) {
            throw new IpGeoException("IP接口查询超时", e);
        }

        if (responseCode != HttpURLConnection.HTTP_OK) {
            throw new IpGeoException("IP接口请求失败，响应码: " + responseCode);
        }

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            return response.toString();
        } finally {
            conn.disconnect();
        }
    }

    /**
     * 转换百度API响应为自定义Ip对象
     */
    private Ip convertToIp(BaiduIpResponse baiduResponse) {
        Ip ip = new Ip();
        ip.setIp(baiduResponse.getIp());

        if (baiduResponse.getData() instanceof Map<?, ?> dataMap) {
            ip
                    .setContinent((String) dataMap.get("continent"))
                    .setCountry((String) dataMap.get("country"))
                    .setProvince((String) dataMap.get("prov"))
                    .setCity((String) dataMap.get("city"))
                    .setDistrict((String) dataMap.get("district"))
                    .setZipcode((String) dataMap.get("zipcode"))
                    .setOwner((String) dataMap.get("owner"))
                    .setIsp((String) dataMap.get("isp"))
                    .setAdcode((String) dataMap.get("adcode"));
        }
        return ip;
    }

    /**
     * 验证IP有效性
     */
    private boolean isValidIp(String ip) {
        return ip != null && !ip.trim().isEmpty() && !"unknown".equalsIgnoreCase(ip.trim());
    }

    /**
     * 百度API响应实体
     */
    @Data
    private static class BaiduIpResponse {
        private String code;
        private Object data;
        private String ip;
    }

    /**
     * IP地理信息查询异常
     */
    public static class IpGeoException extends Exception {
        public IpGeoException(String message) {
            super(message);
        }
        public IpGeoException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    /**
     * 标准化IP地址（统一本地回环地址格式）
     */
    private String normalizeIp(String ip) {
        if (ip == null) return null;
        // 本地回环地址统一标记（避免缓存无意义的本地IP）
        if (ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")) {
            /* 本地回环地址统一用特殊标识 */
            return "LOCAL_LOOPBACK";
        }

        /* 公网IP保持原样 */
        return ip;
    }
}

